// Copyright (c) 2021 Presto Labs Pte. Ltd.
// Author: jasonyoon

#include "coin2/strategy/linear_model/focus_midfreq.h"

namespace coin2::strategy::linear_model {


int LmSubStrategy2::UpdateSignal(
    int64_t timestamp,
    int64_t* first_feed_time,
    double midret_pred,
    bool clear,
    double sigma,
    double* feats,
    OrderExecutorSystem* oe,
    // PredictionInterval* pred_interval,
    AggregatedSizer2* asizer,
    double* adjust_info,
    double leverage_rate) {
  // return value: number of transactions
  int ret = 0;
  if (!bbo) return ret;
  if (!pholder) return ret;
  CHECK(oe);
  if (!oe->HasProduct(*pholder)) return ret;
  if (!oe->HasPosition(*pholder)) return ret;
  const auto& fconf = focus_midfreq_config;

  if (*first_feed_time == 0L) *first_feed_time = timestamp;
  // do not trade for the first 30 secs
  if (timestamp - *first_feed_time < 30e9) return ret;
  double current_stack = oe->GetCurrentStackAbsolute(*pholder);

  midret_pred *= fconf.signal_multiplier();
  adjust_info[8] = midret_pred;

  BlockInfo block = BlockOrder(timestamp, oe, asizer,
      adjust_info, clear, current_stack, fconf);

  // clear position
  // backward compatibility
  if (fconf.exec_type() == MidfreqExecType::CLEAR_EXEC  ||
      fconf.clear_mode() || clear) {
    return ClearPosition(timestamp, oe, &block, leverage_rate);
  }

  switch (fconf.exec_type()) {
    case MidfreqExecType::ENSEMBLE_EXEC:
    case MidfreqExecType::BBO_EXEC:
      ret += BboExecution(oe, adjust_info, timestamp, midret_pred, sigma,
          current_stack, leverage_rate, block, fconf);
      break;
    case MidfreqExecType::MM_EXEC:
      ret += MmExecution(oe, adjust_info, timestamp, midret_pred, sigma,
          current_stack, leverage_rate, block, fconf, feats);
      break;
    case MidfreqExecType::EXP_EXEC:
      ret += LmExecution(oe, adjust_info, timestamp, midret_pred, sigma,
          current_stack, leverage_rate, block, fconf);
      break;
    case MidfreqExecType::TARGET_POS_BBO_EXEC:
      ret += TargetPosBboExecution(oe, adjust_info, timestamp, midret_pred, sigma,
          current_stack, leverage_rate, block, fconf);
      break;
    case MidfreqExecType::PORTFOLIO_EXEC:
      ret += PortfolioExecution(oe, adjust_info, timestamp, midret_pred, feats,
          leverage_rate, &block, fconf);
    default:
      break;
  }

  oe->onPeriodicOrderCheck(*pholder, timestamp);
  return ret;
}

void LmFocusMidfreqLogic::onFeed(
    const Symbol* symbol,
    const FeedUpdate& upd,
    const ::fastfeature::FeedMessage& feed_msg,
    LinearModel* linear_model,
    int64_t* lm_feed_id,
    const int64_t* global_feed_id,
    LinearModelDelegateProto* delegate,
    bool simulation) {
  if (strats_.count(symbol) > 0) {
    if (feed_msg.has_book()
        && feed_msg.book().ask_size() > 0
        && feed_msg.book().bid_size() > 0) {
      auto ask0 = feed_msg.book().ask(0);
      auto bid0 = feed_msg.book().bid(0);
      auto bbo = Bbo {
          feed_msg.fetched_time(),
          feed_msg.exchange_time(),
          ask0.price_float(),
          bid0.price_float(),
      };
      strats_.at(symbol)->UpdateBbo(bbo);

      double ask0_p = ask0.price_float();
      double bid0_p = bid0.price_float();
      double ask0_pq = ask0_p * ask0.qty_float();
      double bid0_pq = bid0_p * bid0.qty_float();
      strats_.at(symbol)->UpdateBbo2(ask0_p, bid0_p, ask0_pq, bid0_pq, feed_msg.fetched_time());
    }
    strats_.at(symbol)->UpdateProduct(product_cache_, upd.product());
    if (feed_msg.feed_type() == ::fastfeature::TRADE_FEED)
      strats_.at(symbol)->UpdateTrade(
          feed_msg.trade().price_float(),
          feed_msg.trade().qty_float(),
          feed_msg.fetched_time());
  }
  const auto& fconf = lm_product_config_.focus_midfreq_config();

  const Symbol* focus_symbol = symbol;
  if (focus_symbols_.count(symbol)) {
    focus_symbol = focus_symbols_[symbol];
  }

  // check last_inference_t init
  if (!last_inference_t_.count(focus_symbol))
    last_inference_t_[focus_symbol] = 0;
  int64_t last_inference_time = last_inference_t_.at(focus_symbol);

  // for reusing previous compute signal, need signal life span setted
  delegate->ClearProto();
  // inference after last_infer + inference_cool
  bool has_inference = false;
  if (fconf.inference_cool_in_sec() * Time::kNanosecondsPerSecond < feed_msg.fetched_time() - last_inference_time) {
    linear_model->TriggerSingleSymbol(nullptr);
    has_inference = true;
  }

  // LOG(INFO) << linear_model->GetAllFeatures()->DebugString();
  // usually, response is empty, when IsSampled = False
  // option added to reuse previous signal for inference_life_span
  if (delegate->response_.buy_model().empty()) {
    // if inference fail, use last_inference_delegate, if inference life_span not ended
    if (feed_msg.has_book()) {
      if (fconf.inference_life_span_in_sec() * Time::kNanosecondsPerSecond > feed_msg.fetched_time() - last_inference_time) {
        if (last_inference_delegate_.count(focus_symbol)) {
          *delegate = last_inference_delegate_.at(focus_symbol);
        } else {
          return;
        }
      } else {
        // delegate->ClearProto();
        return;
      }
    } else if (feed_msg.has_trade()) {
      return;
    } else {
      return;
    }
  } else {
    // update last_inference
    last_inference_t_.at(focus_symbol) = feed_msg.fetched_time();
    last_inference_delegate_[focus_symbol] = *delegate;
  }

  // check tps
  num_reqs_.UpdateTimeOnly(upd.timestamp());
  const int max_tps = fconf.max_tps();
  if (num_reqs_.sum() > max_tps) return;

  auto signal_resp = delegate->response_.buy_model().Get(0);
  double signal = signal_resp.signal();
  bool clear = signal_resp.clear();
  const int num_models = delegate->response_.buy_model().size();
  double sigma = 0;
  if (fconf.exec_type() == MidfreqExecType::BBO_EXEC &&
      num_models > 1) {
    sigma = delegate->response_.buy_model().Get(1).signal();
  } else if (fconf.use_edge_model() &&
      fconf.exec_type() == MidfreqExecType::MM_EXEC &&
      !delegate->response_.buy_edge_model().empty()) {
    for (int i = 0; i < delegate->response_.buy_edge_model().size(); i++) {
      sigma += delegate->response_.buy_edge_model().Get(i).signal();
    }
    sigma /= delegate->response_.buy_edge_model().size();
  } else if (fconf.exec_type() == MidfreqExecType::MM_EXEC) {
    sigma = fconf.static_sigma_bps();
  }

  if (num_models > 1 &&
      (fconf.exec_type() == MidfreqExecType::TARGET_POS_BBO_EXEC ||
       fconf.exec_type() == MidfreqExecType::ENSEMBLE_EXEC ||
      fconf.exec_type() == MidfreqExecType::MM_EXEC)) {
    for (int i = 1; i < num_models; i++) {
      signal += delegate->response_.buy_model().Get(i).signal();
      clear |= signal_resp.clear();
    }
    signal /= num_models;
  }

  // signal_window update for one_config sim
  if (strats_.count(symbol) > 0) {
    strats_[symbol]->UpdateSignalWindow(feed_msg.fetched_time(), std::abs(signal));
    aggr_signal_[symbol] = strats_[symbol]->GetSignalMean();
    if (last_signal_mean_update_t_ + 5'000'000'000LL < upd.timestamp()) {
      last_signal_mean_update_t_ = upd.timestamp();
      last_signal_mean = unordered_map_valid_mean(aggr_signal_);
    }
    strats_[symbol]->signal_aggr_moving_mean = last_signal_mean;
    if (fconf.use_symbol_signal_neut()) {
      if (clear) {
        symbol_signal_mean.Ban(focus_symbol_idx[symbol]);
      } else {
        symbol_signal_mean.Update(focus_symbol_idx[symbol], signal);
      }
      signal = signal - symbol_signal_mean.mean();
    }
  }

  if (!focus_symbols_.count(symbol)) {
    auto focus_symbolstr = signal_resp.symbol();
    const Symbol* focus_symbol_set = linear_model->GetFeatureManager()
      ->GetSymbolManager()->FromSymbolString(focus_symbolstr);
    focus_symbols_[symbol] = focus_symbol_set;
    LOG(INFO) << fmt::format("symbol : {}, focus_symbol_set : {}",
      symbol->symbol_string(), focus_symbol_set->symbol_string());
  }

  // load feature values
  if (feature_index_.empty()) {
    feature_index_.resize(NUM_COLUMNS);
    feature_values_.resize(NUM_COLUMNS);
    GetIndex(linear_model, "FocusMidfreq.premium_mean_scale", MEAN_SCALE);
    GetIndex(linear_model, "FocusMidfreq.premium_std", STD);
    GetIndex(linear_model, "FocusMidfreq.premium_ref_askp", REF_ASKP);
    GetIndex(linear_model, "FocusMidfreq.premium_ref_bidp", REF_BIDP);
    GetIndex(linear_model, "FocusMidfreq.debug_midp", DEBUG_MIDP);
    GetIndex(linear_model, "FocusMidfreq.target_position", TARGET_POSITION);
  }

  // get target position / time remaining from lm
  if (fconf.exec_type() == MidfreqExecType::PORTFOLIO_EXEC) {
    const auto& res = delegate->response_.buy_model();
    // for (int32_t i = 0; i < res.size(); ++i) {
    //   LOG(INFO) << fmt::format("res {}: {}", i + 1, res.Get(i).DebugString());
    // }

    CHECK_GE(res.size(), 3);
    // there is valid min / max for signal value (+-1e6)
    // however, qty position can easily exceed it.
    // therefore we divide and multiply 1e9 for position signal
    // reference: https://github.com/prestolabs/fastfeature/blob/
    // 68f53b9e32c8266c16dfca57d0beae8b398dd0bc/fastfeature/csrc/
    // src/presto/quant/feature/linear_model/linear_model.cc#L214-L222
    feature_values_[TARGET_POSITION] = fconf.max_abspos_quote() * res.Get(1).signal() * 1e9;
    feature_values_[TIME_REMAINING] = res.Get(2).signal();
  } else {
    for (int i = 0; i < NUM_COLUMNS; ++i) {
      if (feature_index_[i] >= 0)
        feature_values_[i] =
          linear_model->GetAllFeatures()->GetValue(feature_index_[i]);
      else
        feature_values_[i] = 0;
    }
  }

  if (strats_.count(focus_symbol) > 0) {
    // get date str
    struct tm timestruct;
    time_t seconds = upd.timestamp() / 1e9;
    gmtime_r(&seconds, &timestruct);
    int year = timestruct.tm_year + 1900;
    int month = timestruct.tm_mon + 1;
    int day = timestruct.tm_mday;
    std::string date_str = std::to_string(year * 10000 + month * 100 + day);

    // update adjust info path if date is changed
    if (date_str > last_date_str_) {
      CloseAdjustInfo();
      OpenAdjustInfo(date_str);
      if (model_feats_writer_.has_value()) {
        if (!num_model_feats_) {
          model_feat_keys_ = linear_model->GetCalcFeats();
          num_model_feats_ = model_feat_keys_.size();
          model_feats_.resize(num_model_feats_);
        }
        *model_feats_writer_ << "symbol,timestamp";
        for (int i = 0; i < num_model_feats_; ++i) {
          *model_feats_writer_ << fmt::format(",{}", model_feat_keys_[i]);
        }
        *model_feats_writer_ << "\n";
      }
    }

    // initialize adjust_info
    std::fill(adjust_info_.begin(), adjust_info_.end(), 0);

    int reqs = strats_[focus_symbol]->UpdateSignal(
        upd.timestamp(),
        &first_feed_time_,
        signal,
        clear,
        sigma,
        &feature_values_[0],
        oe_,
        // &pred_interval_,
        &asizer_,
        &adjust_info_[0],
        lm_product_config_.leverage_rate());
    num_reqs_.Update(upd.timestamp(), reqs);

    // init mark t
    if (!last_mark_t_.count(focus_symbol))
      last_mark_t_[focus_symbol] = 0;

    // 15 columns

    if (!clear && (adjust_info_writer_.has_value() && adjust_info_[0] > 0) &&
        (last_mark_t_[focus_symbol] + fconf.adjust_info_cooltime_sec() * 1'000'000'000LL < upd.timestamp() ||reqs > 0)) {
      *adjust_info_writer_
        << fmt::format("{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{}\n",
            focus_symbol->symbol_string().c_str(), upd.timestamp(),
            adjust_info_[0], adjust_info_[1],
            adjust_info_[2], adjust_info_[3], adjust_info_[4], adjust_info_[5],
            adjust_info_[6], adjust_info_[7], adjust_info_[8], adjust_info_[9],
            adjust_info_[10], adjust_info_[11], adjust_info_[12],
            adjust_info_[13], adjust_info_[14], adjust_info_[15]);
      if (has_inference) {
        linear_model->GetCalcData(&model_feats_[0]);
        *model_feats_writer_ << fmt::format("{},{}",
          focus_symbol->symbol_string().c_str(), upd.timestamp());
        for (int i = 0; i < num_model_feats_; ++i) {
          *model_feats_writer_ << fmt::format(",{}", model_feats_[i]);
        }
        *model_feats_writer_ << "\n";
      }
      last_mark_t_[focus_symbol] = upd.timestamp();
    }
  }
}

void SearchLmFocusMidfreqLogic::onFeed(
    const Symbol* symbol,
    const FeedUpdate& upd,
    const ::fastfeature::FeedMessage& feed_msg,
    LinearModel* linear_model,
    int64_t* lm_feed_id,
    const int64_t* global_feed_id,
    LinearModelDelegateProto* delegate,
    bool simulation) {
  CHECK(simulation);
  if (strats_.count(symbol) > 0) {
    if (feed_msg.has_book()
        && feed_msg.book().ask_size() > 0
        && feed_msg.book().bid_size() > 0) {
      auto ask0 = feed_msg.book().ask(0);
      auto bid0 = feed_msg.book().bid(0);
      auto bbo = Bbo {
          feed_msg.fetched_time(),
          feed_msg.exchange_time(),
          ask0.price_float(),
          bid0.price_float(),
      };
      for (auto&& [sim_account_id_, substrat] : strats_.at(symbol)) {
        (void)sim_account_id_;
        substrat->UpdateBbo(bbo);
      }

      double ask0_p = ask0.price_float();
      double bid0_p = bid0.price_float();
      double ask0_pq = ask0_p * ask0.qty_float();
      double bid0_pq = bid0_p * bid0.qty_float();
      for (auto&& [sim_account_id_, substrat] : strats_.at(symbol)) {
        (void)sim_account_id_;
        substrat->UpdateBbo2(ask0_p, bid0_p, ask0_pq, bid0_pq, feed_msg.fetched_time());
        break; // shared moving window
      }
    }
    for (auto&& [sim_account_id_, substrat] : strats_.at(symbol)) {
      (void)sim_account_id_;
      substrat->UpdateProduct(product_cache_, upd.product());
    }
    if (feed_msg.feed_type() == ::fastfeature::TRADE_FEED) {
      for (auto&& [sim_account_id_, substrat] : strats_.at(symbol)) {
        (void)sim_account_id_;
        substrat->UpdateTrade(
            feed_msg.trade().price_float(),
            feed_msg.trade().qty_float(),
            feed_msg.fetched_time());
        break; // shared moving window
      }
    }
  }

  const auto& fconf = lm_product_config_.search_focus_midfreq_config(0);

  const Symbol* focus_symbol = symbol;
  if (focus_symbols_.count(symbol)) {
    focus_symbol = focus_symbols_[symbol];
  }
  // check last_inference_t init
  if (!last_inference_t_.count(focus_symbol))
    last_inference_t_[focus_symbol] = 0;
  int64_t last_inference_time = last_inference_t_.at(focus_symbol);

  delegate->ClearProto();
  // inference after last_infer + inference_cool
  bool has_inference = false;
  if (fconf.inference_cool_in_sec() * Time::kNanosecondsPerSecond < feed_msg.fetched_time() - last_inference_time) {
    linear_model->TriggerSingleSymbol(nullptr);
    has_inference = true;
  }

  // LOG(INFO) << linear_model->GetAllFeatures()->DebugString();
  if (delegate->response_.buy_model().empty()) {
    // if inference fail, use last_inference_delegate, if inference life_span not ended
    if (feed_msg.has_book()) {
      if (fconf.inference_life_span_in_sec() * Time::kNanosecondsPerSecond > feed_msg.fetched_time() - last_inference_time) {
        if (last_inference_delegate_.count(focus_symbol)) {
          *delegate = last_inference_delegate_.at(focus_symbol);
        } else {
          return;
        }
      } else {
        // delegate->ClearProto();
        return;
      }
    } else if (feed_msg.has_trade()) {
      return;
    } else {
      return;
    }
  } else {
    // update last_inference
    last_inference_t_.at(focus_symbol) = feed_msg.fetched_time();
    last_inference_delegate_[focus_symbol] = *delegate;
  }

  for (auto& num_req_ : num_reqs_) {
    num_req_.UpdateTimeOnly(upd.timestamp());
  }
  const int max_tps = fconf.max_tps();

  auto signal_resp = delegate->response_.buy_model().Get(0);
  double signal = signal_resp.signal();
  bool clear = signal_resp.clear();
  const int num_models = delegate->response_.buy_model().size();
  double sigma = 0;
  if (fconf.exec_type() == MidfreqExecType::BBO_EXEC &&
      num_models > 1) {
    sigma = delegate->response_.buy_model().Get(1).signal();
  } else if (fconf.use_edge_model() &&
      fconf.exec_type() == MidfreqExecType::MM_EXEC &&
      !delegate->response_.buy_edge_model().empty()) {
    for (int i = 0; i < delegate->response_.buy_edge_model().size(); i++) {
      sigma += delegate->response_.buy_edge_model().Get(i).signal();
    }
    sigma /= delegate->response_.buy_edge_model().size();
  } else if (fconf.exec_type() == MidfreqExecType::MM_EXEC) {
    sigma = fconf.static_sigma_bps();
  }

  if (num_models > 1 &&
      (fconf.exec_type() == MidfreqExecType::TARGET_POS_BBO_EXEC ||
       fconf.exec_type() == MidfreqExecType::ENSEMBLE_EXEC ||
      fconf.exec_type() == MidfreqExecType::MM_EXEC)) {
    for (int i = 1; i < num_models; i++) {
      signal += delegate->response_.buy_model().Get(i).signal();
      clear |= signal_resp.clear();
    }
    signal /= num_models;
  }

  // signal_window update for Multi_exec_config search
  if (signal_.count(symbol) > 0 && fconf.signal_window_in_sec() > 0) {
    signal_[symbol]->Update(feed_msg.fetched_time(), std::abs(signal));
    aggr_signal_[symbol] = signal_[symbol]->average();
  }

  if (strats_.count(symbol) > 0 && fconf.use_symbol_signal_neut()) {
    if (clear) {
      symbol_signal_mean.Ban(focus_symbol_idx[symbol]);
    } else {
      symbol_signal_mean.Update(focus_symbol_idx[symbol], signal);
    }
    signal = signal - symbol_signal_mean.mean();
  }

  if (!focus_symbols_.count(symbol)) {
    auto focus_symbolstr = signal_resp.symbol();
    const Symbol* focus_symbol_set = linear_model->GetFeatureManager()
      ->GetSymbolManager()->FromSymbolString(focus_symbolstr);
    focus_symbols_[symbol] = focus_symbol_set;
    LOG(INFO) << fmt::format("symbol : {}, focus_symbol_set : {}",
      symbol->symbol_string(), focus_symbol_set->symbol_string());
  }

  // load feature values
  if (feature_index_.empty()) {
    feature_index_.resize(NUM_COLUMNS);
    feature_values_.resize(NUM_COLUMNS);
    GetIndex(linear_model, "FocusMidfreq.premium_mean_scale", MEAN_SCALE);
    GetIndex(linear_model, "FocusMidfreq.premium_std", STD);
    GetIndex(linear_model, "FocusMidfreq.premium_ref_askp", REF_ASKP);
    GetIndex(linear_model, "FocusMidfreq.premium_ref_bidp", REF_BIDP);
    GetIndex(linear_model, "FocusMidfreq.debug_midp", DEBUG_MIDP);
    GetIndex(linear_model, "FocusMidfreq.target_position", TARGET_POSITION);
  }

  // get target position / time remaining from lm
  if (fconf.exec_type() == MidfreqExecType::PORTFOLIO_EXEC) {
    const auto& res = delegate->response_.buy_model();
    // for (int32_t i = 0; i < res.size(); ++i) {
    //   LOG(INFO) << fmt::format("res {}: {}", i + 1, res.Get(i).DebugString());
    // }

    CHECK_GE(res.size(), 3);
    // there is valid min / max for signal value (+-1e6)
    // however, qty position can easily exceed it.
    // therefore we divide and multiply 1e9 for position signal
    // reference: https://github.com/prestolabs/fastfeature/blob/
    // 68f53b9e32c8266c16dfca57d0beae8b398dd0bc/fastfeature/csrc/
    // src/presto/quant/feature/linear_model/linear_model.cc#L214-L222
    feature_values_[TARGET_POSITION] = fconf.max_abspos_quote() * res.Get(1).signal() * 1e9;
    feature_values_[TIME_REMAINING] = res.Get(2).signal();
  } else {
    for (int i = 0; i < NUM_COLUMNS; ++i) {
      if (feature_index_[i] >= 0)
        feature_values_[i] =
          linear_model->GetAllFeatures()->GetValue(feature_index_[i]);
      else
        feature_values_[i] = 0;
    }
  }

  if (strats_.count(focus_symbol) > 0) {
    // get date str
    struct tm timestruct;
    time_t seconds = upd.timestamp() / 1e9;
    gmtime_r(&seconds, &timestruct);
    int year = timestruct.tm_year + 1900;
    int month = timestruct.tm_mon + 1;
    int day = timestruct.tm_mday;
    std::string date_str = std::to_string(year * 10000 + month * 100 + day);

    // update adjust info path if date is changed
    if (date_str > last_date_str_) {
      CloseAdjustInfo();
      OpenAdjustInfo(date_str);
      if (model_feats_writer_.has_value()) {
        if (!num_model_feats_) {
          model_feat_keys_ = linear_model->GetCalcFeats();
          num_model_feats_ = model_feat_keys_.size();
          model_feats_.resize(num_model_feats_);
        }
        *model_feats_writer_ << "symbol,timestamp";
        for (int i = 0; i < num_model_feats_; ++i) {
          *model_feats_writer_ << fmt::format(",{}", model_feat_keys_[i]);
        }
        *model_feats_writer_ << "\n";
      }
    }

    // initialize adjust_info

    int feature_reqs = 0;
    for (int i = 0; i < oes_.size(); i++) {
      if (num_reqs_[i].sum() > max_tps) {
        continue;
      }
      std::fill(adjust_info_.begin(), adjust_info_.end(), 0);

      auto sim_account_id_ = sim_account_ids_[i];
      int reqs = 0;
      if (strats_[focus_symbol].count(sim_account_id_) > 0) {
        if (last_signal_mean_update_t_ + 5'000'000'000LL < upd.timestamp()) {
          last_signal_mean_update_t_ = upd.timestamp();
          last_signal_mean = unordered_map_valid_mean(aggr_signal_);
        }
        strats_[focus_symbol][sim_account_id_]->signal_aggr_moving_mean = last_signal_mean;
        reqs = strats_[focus_symbol][sim_account_id_]->UpdateSignal(
            upd.timestamp(),
            &first_feed_time_,
            signal,
            clear,
            sigma,
            &feature_values_[0],
            oes_[i],
            // &pred_interval_,
            &asizers_[i],
            &adjust_info_[0],
            lm_product_config_.leverage_rate());
        num_reqs_[i].Update(upd.timestamp(), reqs);

        // init mark t
        if (!last_mark_ts_[i].count(focus_symbol))
          last_mark_ts_[i][focus_symbol] = 0;

        if (!clear && (!adjust_info_writers_.empty() && adjust_info_[0] > 0) &&
            (last_mark_ts_[i][focus_symbol] + fconf.adjust_info_cooltime_sec() * 1'000'000'000LL < upd.timestamp() || reqs > 0)) {
          *adjust_info_writers_[i]
            << fmt::format("{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{}\n",
                focus_symbol->symbol_string().c_str(), upd.timestamp(),
                adjust_info_[0], adjust_info_[1],
                adjust_info_[2], adjust_info_[3], adjust_info_[4], adjust_info_[5],
                adjust_info_[6], adjust_info_[7], adjust_info_[8], adjust_info_[9],
                adjust_info_[10], adjust_info_[11], adjust_info_[12],
                adjust_info_[13], adjust_info_[14], adjust_info_[15]);
          last_mark_ts_[i][focus_symbol] = upd.timestamp();
        }
      }
      feature_reqs += reqs;
    }

    if (!last_mark_feature_t_.count(focus_symbol))
      last_mark_feature_t_[focus_symbol] = 0;

    if (!clear && has_inference && (model_feats_writer_.has_value() && adjust_info_[0] > 0) &&
        (last_mark_feature_t_[focus_symbol] + fconf.adjust_info_cooltime_sec() * 1'000'000'000LL < upd.timestamp() || feature_reqs > 0)) {
      linear_model->GetCalcData(&model_feats_[0]);
      *model_feats_writer_ << fmt::format("{},{}",
        focus_symbol->symbol_string().c_str(), upd.timestamp());
      for (int i = 0; i < num_model_feats_; ++i) {
        *model_feats_writer_ << fmt::format(",{}", model_feats_[i]);
      }
      *model_feats_writer_ << "\n";
      last_mark_feature_t_[focus_symbol] = upd.timestamp();
    }
  }
}

}  // namespace coin2::strategy::linear_model
